honk.go (view raw)
1//
2// Copyright (c) 2019 Ted Unangst <tedu@tedunangst.com>
3//
4// Permission to use, copy, modify, and distribute this software for any
5// purpose with or without fee is hereby granted, provided that the above
6// copyright notice and this permission notice appear in all copies.
7//
8// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15
16package main
17
18import (
19 "flag"
20 "fmt"
21 "html/template"
22 golog "log"
23 "log/syslog"
24 notrand "math/rand"
25 "os"
26 "strconv"
27 "strings"
28 "time"
29
30 "humungus.tedunangst.com/r/webs/httpsig"
31 "humungus.tedunangst.com/r/webs/log"
32)
33
34var softwareVersion = "develop"
35
36func init() {
37 notrand.Seed(time.Now().Unix())
38}
39
40type WhatAbout struct {
41 ID int64
42 Name string
43 Display string
44 About string
45 HTAbout template.HTML
46 Onts []string
47 Key string
48 URL string
49 Options UserOptions
50 SecKey httpsig.PrivateKey
51}
52
53type UserOptions struct {
54 SkinnyCSS bool `json:",omitempty"`
55 OmitImages bool `json:",omitempty"`
56 MentionAll bool `json:",omitempty"`
57 InlineQuotes bool `json:",omitempty"`
58 Avatar string `json:",omitempty"`
59 Banner string `json:",omitempty"`
60 MapLink string `json:",omitempty"`
61 Reaction string `json:",omitempty"`
62 MeCount int64
63 ChatCount int64
64}
65
66type KeyInfo struct {
67 keyname string
68 seckey httpsig.PrivateKey
69}
70
71const serverUID int64 = -2
72const readyLuserOne int64 = 1
73
74type Honk struct {
75 ID int64
76 UserID int64
77 Username string
78 What string
79 Honker string
80 Handle string
81 Handles string
82 Oonker string
83 Oondle string
84 XID string
85 RID string
86 Date time.Time
87 URL string
88 Noise string
89 Precis string
90 Format string
91 Convoy string
92 Audience []string
93 Public bool
94 Whofore int64
95 Replies []*Honk
96 Flags int64
97 HTPrecis template.HTML
98 HTML template.HTML
99 Style string
100 Open string
101 Donks []*Donk
102 Onts []string
103 Place *Place
104 Time *Time
105 Mentions []Mention
106 Badonks []Badonk
107}
108
109type Badonk struct {
110 Who string
111 What string
112}
113
114type Chonk struct {
115 ID int64
116 UserID int64
117 XID string
118 Who string
119 Target string
120 Date time.Time
121 Noise string
122 Format string
123 Donks []*Donk
124 Handle string
125 HTML template.HTML
126}
127
128type Chatter struct {
129 Target string
130 Chonks []*Chonk
131}
132
133type Mention struct {
134 Who string
135 Where string
136}
137
138func (mention *Mention) IsPresent(noise string) bool {
139 nick := strings.TrimLeft(mention.Who, "@")
140 idx := strings.IndexByte(nick, '@')
141 if idx != -1 {
142 nick = nick[:idx]
143 }
144 return strings.Contains(noise, ">@"+nick) || strings.Contains(noise, "@<span>"+nick)
145}
146
147type OldRevision struct {
148 Precis string
149 Noise string
150}
151
152const (
153 flagIsAcked = 1
154 flagIsBonked = 2
155 flagIsSaved = 4
156 flagIsUntagged = 8
157 flagIsReacted = 16
158)
159
160func (honk *Honk) IsAcked() bool {
161 return honk.Flags&flagIsAcked != 0
162}
163
164func (honk *Honk) IsBonked() bool {
165 return honk.Flags&flagIsBonked != 0
166}
167
168func (honk *Honk) IsSaved() bool {
169 return honk.Flags&flagIsSaved != 0
170}
171
172func (honk *Honk) IsUntagged() bool {
173 return honk.Flags&flagIsUntagged != 0
174}
175
176func (honk *Honk) IsReacted() bool {
177 return honk.Flags&flagIsReacted != 0
178}
179
180type Donk struct {
181 FileID int64
182 XID string
183 Name string
184 Desc string
185 URL string
186 Media string
187 Local bool
188 External bool
189}
190
191type Place struct {
192 Name string
193 Latitude float64
194 Longitude float64
195 Url string
196}
197
198type Duration int64
199
200func (d Duration) String() string {
201 s := time.Duration(d).String()
202 if strings.HasSuffix(s, "m0s") {
203 s = s[:len(s)-2]
204 }
205 if strings.HasSuffix(s, "h0m") {
206 s = s[:len(s)-2]
207 }
208 return s
209}
210
211func parseDuration(s string) time.Duration {
212 didx := strings.IndexByte(s, 'd')
213 if didx != -1 {
214 days, _ := strconv.ParseInt(s[:didx], 10, 0)
215 dur, _ := time.ParseDuration(s[didx:])
216 return dur + 24*time.Hour*time.Duration(days)
217 }
218 dur, _ := time.ParseDuration(s)
219 return dur
220}
221
222type Time struct {
223 StartTime time.Time
224 EndTime time.Time
225 Duration Duration
226}
227
228type Honker struct {
229 ID int64
230 UserID int64
231 Name string
232 XID string
233 Handle string
234 Flavor string
235 Combos []string
236 Meta HonkerMeta
237}
238
239type HonkerMeta struct {
240 Notes string
241}
242
243type SomeThing struct {
244 What int
245 XID string
246 Owner string
247 Name string
248}
249
250const (
251 SomeNothing int = iota
252 SomeActor
253 SomeCollection
254)
255
256var serverName string
257var serverPrefix string
258var masqName string
259var dataDir = "."
260var viewDir = "."
261var iconName = "icon.png"
262var serverMsg template.HTML
263var aboutMsg template.HTML
264var loginMsg template.HTML
265
266func ElaborateUnitTests() {
267}
268
269func unplugserver(hostname string) {
270 db := opendatabase()
271 xid := fmt.Sprintf("%%https://%s/%%", hostname)
272 db.Exec("delete from honkers where xid like ? and flavor = 'dub'", xid)
273 db.Exec("delete from doovers where rcpt like ?", xid)
274}
275
276func reexecArgs(cmd string) []string {
277 args := []string{"-datadir", dataDir}
278 args = append(args, log.Args()...)
279 args = append(args, cmd)
280 return args
281}
282
283var elog, ilog, dlog *golog.Logger
284
285func main() {
286 flag.StringVar(&dataDir, "datadir", dataDir, "data directory")
287 flag.StringVar(&viewDir, "viewdir", viewDir, "view directory")
288 flag.Parse()
289
290 log.Init(log.Options{Progname: "honk", Facility: syslog.LOG_UUCP})
291 elog = log.E
292 ilog = log.I
293 dlog = log.D
294
295 args := flag.Args()
296 cmd := "run"
297 if len(args) > 0 {
298 cmd = args[0]
299 }
300 switch cmd {
301 case "init":
302 initdb()
303 case "upgrade":
304 upgradedb()
305 case "version":
306 fmt.Println(softwareVersion)
307 os.Exit(0)
308 }
309 db := opendatabase()
310 dbversion := 0
311 getconfig("dbversion", &dbversion)
312 if dbversion != myVersion {
313 elog.Fatal("incorrect database version. run upgrade.")
314 }
315 getconfig("servermsg", &serverMsg)
316 getconfig("aboutmsg", &aboutMsg)
317 getconfig("loginmsg", &loginMsg)
318 getconfig("servername", &serverName)
319 getconfig("masqname", &masqName)
320 if masqName == "" {
321 masqName = serverName
322 }
323 serverPrefix = fmt.Sprintf("https://%s/", serverName)
324 getconfig("usersep", &userSep)
325 getconfig("honksep", &honkSep)
326 getconfig("devel", &develMode)
327 getconfig("fasttimeout", &fastTimeout)
328 getconfig("slowtimeout", &slowTimeout)
329 getconfig("signgets", &signGets)
330 prepareStatements(db)
331 switch cmd {
332 case "admin":
333 adminscreen()
334 case "import":
335 if len(args) != 4 {
336 elog.Fatal("import username mastodon|twitter srcdir")
337 }
338 importMain(args[1], args[2], args[3])
339 case "devel":
340 if len(args) != 2 {
341 elog.Fatal("need an argument: devel (on|off)")
342 }
343 switch args[1] {
344 case "on":
345 setconfig("devel", 1)
346 case "off":
347 setconfig("devel", 0)
348 default:
349 elog.Fatal("argument must be on or off")
350 }
351 case "setconfig":
352 if len(args) != 3 {
353 elog.Fatal("need an argument: setconfig key val")
354 }
355 var val interface{}
356 var err error
357 if val, err = strconv.Atoi(args[2]); err != nil {
358 val = args[2]
359 }
360 setconfig(args[1], val)
361 case "adduser":
362 adduser()
363 case "deluser":
364 if len(args) < 2 {
365 fmt.Printf("usage: honk deluser username\n")
366 return
367 }
368 deluser(args[1])
369 case "chpass":
370 if len(args) < 2 {
371 fmt.Printf("usage: honk chpass username\n")
372 return
373 }
374 chpass(args[1])
375 case "follow":
376 if len(args) < 3 {
377 fmt.Printf("usage: honk follow username url\n")
378 return
379 }
380 user, err := butwhatabout(args[1])
381 if err != nil {
382 fmt.Printf("user not found\n")
383 return
384 }
385 var meta HonkerMeta
386 mj, _ := jsonify(&meta)
387 honkerid, err := savehonker(user, args[2], "", "presub", "", mj)
388 if err != nil {
389 fmt.Printf("had some trouble with that: %s\n", err)
390 return
391 }
392 followyou(user, honkerid, true)
393 case "unfollow":
394 if len(args) < 3 {
395 fmt.Printf("usage: honk unfollow username url\n")
396 return
397 }
398 user, err := butwhatabout(args[1])
399 if err != nil {
400 fmt.Printf("user not found\n")
401 return
402 }
403 row := db.QueryRow("select honkerid from honkers where xid = ? and userid = ? and flavor in ('sub')", args[2], user.ID)
404 var honkerid int64
405 err = row.Scan(&honkerid)
406 if err != nil {
407 fmt.Printf("sorry couldn't find them\n")
408 return
409 }
410 unfollowyou(user, honkerid, true)
411 case "cleanup":
412 arg := "30"
413 if len(args) > 1 {
414 arg = args[1]
415 }
416 cleanupdb(arg)
417 case "unplug":
418 if len(args) < 2 {
419 fmt.Printf("usage: honk unplug servername\n")
420 return
421 }
422 name := args[1]
423 unplugserver(name)
424 case "backup":
425 if len(args) < 2 {
426 fmt.Printf("usage: honk backup dirname\n")
427 return
428 }
429 name := args[1]
430 svalbard(name)
431 case "ping":
432 if len(args) < 3 {
433 fmt.Printf("usage: honk ping (from username) (to username or url)\n")
434 return
435 }
436 name := args[1]
437 targ := args[2]
438 user, err := butwhatabout(name)
439 if err != nil {
440 elog.Printf("unknown user")
441 return
442 }
443 ping(user, targ)
444 case "run":
445 serve()
446 case "backend":
447 backendServer()
448 case "test":
449 ElaborateUnitTests()
450 default:
451 elog.Fatal("unknown command")
452 }
453}