tangled
alpha
login
or
join now
zenfyr.dev
/
xpost
2
fork
atom
social media crossposting tool. 3rd time's the charm
mastodon
misskey
crossposting
bluesky
2
fork
atom
overview
issues
1
pulls
pipelines
migrate stuff to PostRef
zenfyr.dev
1 month ago
19170b82
5acecfcf
verified
This commit was signed with the committer's
known signature
.
zenfyr.dev
SSH Key Fingerprint:
SHA256:TtcIcnTnoAB5mqHofsaOxIgiMzfVBxej1AXT7DQdrTE=
+67
-52
7 changed files
expand all
collapse all
unified
split
bluesky
input.py
cross
post.py
service.py
mastodon
input.py
output.py
misskey
input.py
util
dummy.py
+8
-5
bluesky/input.py
···
18
18
RemoteUrlAttachment,
19
19
)
20
20
from cross.media import Blob, download_blob
21
21
-
from cross.post import Post
21
21
+
from cross.post import Post, PostRef
22
22
from cross.service import InputService
23
23
from database.connection import DatabasePool
24
24
from util.util import normalize_service_url
···
77
77
return
78
78
79
79
tokens = tokenize_post(record["text"], record.get('facets', {}))
80
80
-
post = Post(id=post_uri, parent_id=parent_uri, tokens=tokens)
80
80
+
post = Post(id=post_uri, author=self.did, service=self.url, parent_id=parent_uri, tokens=tokens)
81
81
82
82
did, _, rid = AtUri.record_uri(post_uri)
83
83
post.attachments.put(
···
194
194
}
195
195
)
196
196
197
197
+
repost_ref = PostRef(id=post_uri, author=self.did, service=self.url)
198
198
+
reposted_ref = PostRef(id=reposted_uri, author=self.did, service=self.url)
197
199
for out in self.outputs:
198
198
-
self.submitter(lambda: out.accept_repost(post_uri, reposted_uri))
200
200
+
self.submitter(lambda: out.accept_repost(repost_ref, reposted_ref))
199
201
200
202
def _on_delete_post(self, post_id: str, repost: bool):
201
203
post = self._get_post(self.url, self.did, post_id)
202
204
if not post:
203
205
return
204
206
207
207
+
post_ref = PostRef(id=post_id, author=self.did, service=self.url)
205
208
if repost:
206
209
for output in self.outputs:
207
207
-
self.submitter(lambda: output.delete_repost(post_id))
210
210
+
self.submitter(lambda: output.delete_repost(post_ref))
208
211
else:
209
212
for output in self.outputs:
210
210
-
self.submitter(lambda: output.delete_post(post_id))
213
213
+
self.submitter(lambda: output.delete_post(post_ref))
211
214
self._delete_post_by_id(post["id"])
212
215
213
216
+7
-3
cross/post.py
···
25
25
def __repr__(self) -> str:
26
26
return f"AttachmentKeeper(_map={self._map.values()})"
27
27
28
28
-
29
29
-
@dataclass
30
30
-
class Post:
28
28
+
@dataclass(kw_only=True)
29
29
+
class PostRef:
31
30
id: str
31
31
+
author: str
32
32
+
service: str
33
33
+
34
34
+
@dataclass(kw_only=True)
35
35
+
class Post(PostRef):
32
36
parent_id: str | None
33
37
tokens: list[Token]
34
38
text_type: str = "text/plain"
+9
-9
cross/service.py
···
3
3
from abc import ABC, abstractmethod
4
4
from typing import Any, Callable, cast
5
5
6
6
-
from cross.post import Post
6
6
+
from cross.post import Post, PostRef
7
7
from database.connection import DatabasePool
8
8
9
9
columns: list[str] = [
···
132
132
133
133
134
134
class OutputService(Service):
135
135
-
def accept_post(self, service: str, user: str, post: Post):
135
135
+
def accept_post(self, post: Post):
136
136
self.log.warning("NOT IMPLEMENTED (%s), accept_post %s", self.url, post.id)
137
137
138
138
-
def delete_post(self, service: str, user: str, post_id: str):
139
139
-
self.log.warning("NOT IMPLEMENTED (%s), delete_post %s", self.url, post_id)
138
138
+
def delete_post(self, post: PostRef):
139
139
+
self.log.warning("NOT IMPLEMENTED (%s), delete_post %s", self.url, post.id)
140
140
141
141
-
def accept_repost(self, service: str, user: str, repost_id: str, reposted_id: str):
141
141
+
def accept_repost(self, repost: PostRef, reposted: PostRef):
142
142
self.log.warning(
143
143
"NOT IMPLEMENTED (%s), accept_repost %s of %s",
144
144
self.url,
145
145
-
repost_id,
146
146
-
reposted_id,
145
145
+
repost.id,
146
146
+
reposted.id,
147
147
)
148
148
149
149
-
def delete_repost(self, service: str, user: str, repost_id: str):
150
150
-
self.log.warning("NOT IMPLEMENTED (%s), delete_repost %s", self.url, repost_id)
149
149
+
def delete_repost(self, repost: PostRef):
150
150
+
self.log.warning("NOT IMPLEMENTED (%s), delete_repost %s", self.url, repost.id)
151
151
152
152
153
153
class InputService(ABC, Service):
+8
-5
mastodon/input.py
···
15
15
SensitiveAttachment,
16
16
)
17
17
from cross.media import Blob, download_blob
18
18
-
from cross.post import Post
18
18
+
from cross.post import Post, PostRef
19
19
from cross.service import InputService
20
20
from database.connection import DatabasePool
21
21
from mastodon.info import MastodonService, validate_and_transform
···
113
113
parser.feed(status["content"])
114
114
tokens = parser.get_result()
115
115
116
116
-
post = Post(id=status["id"], parent_id=in_reply, tokens=tokens)
116
116
+
post = Post(id=status["id"], author=self.user_id, service=self.url, parent_id=in_reply, tokens=tokens)
117
117
118
118
if quote:
119
119
post.attachments.put(QuoteAttachment(quoted_id=quote['id'], quoted_user=self.user_id))
···
183
183
}
184
184
)
185
185
186
186
+
repost_ref = PostRef(id=status["id"], author=self.user_id, service=self.url)
187
187
+
reposted_ref = PostRef(id=reblog["id"], author=self.user_id, service=self.url)
186
188
for out in self.outputs:
187
187
-
self.submitter(lambda: out.accept_repost(status["id"], reblog["id"]))
189
189
+
self.submitter(lambda: out.accept_repost(repost_ref, reposted_ref))
188
190
189
191
def _on_delete_post(self, status_id: str):
190
192
post = self._get_post(self.url, self.user_id, status_id)
191
193
if not post:
192
194
return
193
195
196
196
+
post_ref = PostRef(id=status_id, author=self.user_id, service=self.url)
194
197
if post["reposted_id"]:
195
198
for output in self.outputs:
196
196
-
self.submitter(lambda: output.delete_repost(status_id))
199
199
+
self.submitter(lambda: output.delete_repost(post_ref))
197
200
else:
198
201
for output in self.outputs:
199
199
-
self.submitter(lambda: output.delete_post(status_id))
202
202
+
self.submitter(lambda: output.delete_post(post_ref))
200
203
self._delete_post_by_id(post["id"])
201
204
202
205
def _accept_msg(self, msg: websockets.Data) -> None:
+23
-19
mastodon/output.py
···
9
9
RemoteUrlAttachment,
10
10
SensitiveAttachment,
11
11
)
12
12
-
from cross.post import Post
12
12
+
from cross.post import Post, PostRef
13
13
from cross.service import OutputService
14
14
from database.connection import DatabasePool
15
15
from mastodon.info import InstanceInfo, MastodonService, validate_and_transform
···
48
48
response = self.fetch_instance_info()
49
49
self.instance_info: InstanceInfo = InstanceInfo.from_api(response)
50
50
51
51
-
def accept_post(self, service: str, user: str, post: Post):
51
51
+
@override
52
52
+
def accept_post(self, post: Post):
52
53
new_root_id: int | None = None
53
54
new_parent_id: int | None = None
54
55
55
56
reply_ref: str | None = None
56
57
if post.parent_id:
57
58
thread = self._find_mapped_thread(
58
58
-
post.parent_id, service, user, self.url, self.user_id
59
59
+
post.parent_id, post.service, post.author, self.url, self.user_id
59
60
)
60
61
61
62
if not thread:
···
65
66
66
67
quote = post.attachments.get(QuoteAttachment)
67
68
if quote:
68
68
-
if quote.quoted_user != user:
69
69
+
if quote.quoted_user != post.author:
69
70
self.log.info("Quoted other user, skipping!")
70
71
return
71
72
72
72
-
quoted_post = self._get_post(service, user, quote.quoted_id)
73
73
+
quoted_post = self._get_post(post.service, post.author, quote.quoted_id)
73
74
if not quoted_post:
74
75
self.log.error("Failed to find quoted post in the database!")
75
76
return
···
103
104
if sensitive and sensitive.sensitive:
104
105
pass # TODO
105
106
106
106
-
def delete_post(self, service: str, user: str, post_id: str):
107
107
-
post = self._get_post(service, user, post_id)
108
108
-
if not post:
107
107
+
@override
108
108
+
def delete_post(self, post: PostRef):
109
109
+
db_post = self._get_post(post.service, post.author, post.id)
110
110
+
if not db_post:
109
111
self.log.info("Post not found in db, skipping delete..")
110
112
return
111
113
112
112
-
mappings = self._get_mappings(post["id"], self.url, self.user_id)
114
114
+
mappings = self._get_mappings(db_post["id"], self.url, self.user_id)
113
115
for mapping in mappings[::-1]:
114
116
self.log.info("Deleting '%s'...", mapping["identifier"])
115
117
requests.delete(
···
118
120
)
119
121
self._delete_post_by_id(mapping["id"])
120
122
121
121
-
def accept_repost(self, service: str, user: str, repost_id: str, reposted_id: str):
122
122
-
reposted = self._get_post(service, user, reposted_id)
123
123
-
if not reposted:
123
123
+
@override
124
124
+
def accept_repost(self, repost: PostRef, reposted: PostRef):
125
125
+
original = self._get_post(reposted.service, reposted.author, reposted.id)
126
126
+
if not original:
124
127
self.log.info("Post not found in db, skipping repost..")
125
128
return
126
129
127
127
-
mappings = self._get_mappings(reposted["id"], self.url, self.user_id)
130
130
+
mappings = self._get_mappings(original["id"], self.url, self.user_id)
128
131
if mappings:
129
132
rsp = requests.post(
130
133
f"{self.url}/api/v1/statuses/{mappings[0]['identifier']}/reblog",
···
150
153
inserted = self._get_post(self.url, self.user_id, rsp.json()["id"])
151
154
if not inserted:
152
155
raise ValueError("Inserted post not found!")
153
153
-
self._insert_post_mapping(reposted["id"], inserted["id"])
156
156
+
self._insert_post_mapping(original["id"], inserted["id"])
154
157
155
155
-
def delete_repost(self, service: str, user: str, repost_id: str):
156
156
-
repost = self._get_post(service, user, repost_id)
157
157
-
if not repost:
158
158
+
@override
159
159
+
def delete_repost(self, repost: PostRef):
160
160
+
db_repost = self._get_post(repost.service, repost.author, repost.id)
161
161
+
if not db_repost:
158
162
self.log.info("Repost not found in db, skipping delete..")
159
163
return
160
164
161
161
-
mappings = self._get_mappings(repost["id"], self.url, self.user_id)
162
162
-
rmappings = self._get_mappings(repost["reposted"], self.url, self.user_id)
165
165
+
mappings = self._get_mappings(db_repost["id"], self.url, self.user_id)
166
166
+
rmappings = self._get_mappings(db_repost["reposted"], self.url, self.user_id)
163
167
164
168
if mappings and rmappings:
165
169
self.log.info(
+5
-4
misskey/input.py
···
15
15
SensitiveAttachment,
16
16
)
17
17
from cross.media import Blob, download_blob
18
18
-
from cross.post import Post
18
18
+
from cross.post import Post, PostRef
19
19
from cross.service import InputService
20
20
from database.connection import DatabasePool
21
21
from misskey.info import MisskeyService
···
113
113
114
114
parser = MarkdownParser() # TODO MFM parser
115
115
tokens = parser.parse(note.get("text", ""), tags, handles)
116
116
-
post = Post(id=note["id"], parent_id=reply["id"] if reply else None, tokens=tokens)
116
116
+
post = Post(id=note["id"], author=self.user_id, service=self.url, parent_id=reply["id"] if reply else None, tokens=tokens)
117
117
118
118
post.attachments.put(RemoteUrlAttachment(url=self.url + "/notes/" + note["id"]))
119
119
if renote:
···
180
180
}
181
181
)
182
182
183
183
+
repost_ref = PostRef(id=note["id"], author=self.user_id, service=self.url)
184
184
+
reposted_ref = PostRef(id=renote["id"], author=self.user_id, service=self.url)
183
185
for out in self.outputs:
184
184
-
self.submitter(lambda: out.accept_repost(note["id"], renote["id"]))
186
186
+
self.submitter(lambda: out.accept_repost(repost_ref, reposted_ref))
185
187
186
188
def _accept_msg(self, msg: websockets.Data) -> None:
187
189
data: dict[str, Any] = cast(dict[str, Any], json.loads(msg))
···
191
193
if type == "note" or type == "reply":
192
194
note_body = data["body"]["body"]
193
195
self._on_note(note_body)
194
194
-
return
195
196
196
197
async def _subscribe_to_home(self, ws: websockets.ClientConnection) -> None:
197
198
await ws.send(
+7
-7
util/dummy.py
···
1
1
from typing import override
2
2
-
from cross.post import Post
2
2
+
from cross.post import Post, PostRef
3
3
from cross.service import OutputService
4
4
from database.connection import DatabasePool
5
5
···
17
17
self.log.info("%s", post)
18
18
19
19
@override
20
20
-
def accept_repost(self, repost_id: str, reposted_id: str):
21
21
-
self.log.info("%s, %s", repost_id, reposted_id)
20
20
+
def accept_repost(self, repost: PostRef, reposted: PostRef):
21
21
+
self.log.info("%s, %s", repost.id, reposted.id)
22
22
23
23
@override
24
24
-
def delete_post(self, post_id: str):
25
25
-
self.log.info("%s", post_id)
24
24
+
def delete_post(self, post: PostRef):
25
25
+
self.log.info("%s", post.id)
26
26
27
27
@override
28
28
-
def delete_repost(self, repost_id: str):
29
29
-
self.log.info("%s", repost_id)
28
28
+
def delete_repost(self, repost: PostRef):
29
29
+
self.log.info("%s", repost.id)