all repos — honk @ master

my fork of honk

masto.go (view raw)

  1package main
  2
  3import (
  4	"bytes"
  5	"database/sql"
  6	"fmt"
  7	"log"
  8	"net/http"
  9	"slices"
 10	"strconv"
 11	"strings"
 12	"time"
 13
 14	"humungus.tedunangst.com/r/webs/junk"
 15	"humungus.tedunangst.com/r/webs/login"
 16)
 17
 18type MastoApp struct {
 19	Name         string `db:"clientname"`
 20	RedirectURI  string `db:"redirecturis"`
 21	ClientID     string `db:"clientid"`
 22	ClientSecret string `db:"clientsecret"`
 23	VapidKey     string `db:"vapidkey"`
 24	AuthToken    string `db:"authtoken"`
 25	Scopes       string `db:"scopes"`
 26}
 27
 28func showoauthlogin(rw http.ResponseWriter, r *http.Request) {
 29	templinfo := make(map[string]interface{})
 30	templinfo = getInfo(r)
 31	templinfo["ClientID"] = r.URL.Query().Get("client_id")
 32	templinfo["RedirectURI"] = r.URL.Query().Get("redirect_uri")
 33
 34	if err := readviews.Execute(rw, "oauthlogin.html", templinfo); err != nil {
 35		elog.Println(err)
 36	}
 37}
 38
 39// https://docs.joinmastodon.org/methods/apps/#create
 40func apiapps(rw http.ResponseWriter, r *http.Request) {
 41	if err := r.ParseForm(); err != nil {
 42		http.Error(rw, "invalid input", http.StatusUnprocessableEntity)
 43		elog.Println(err)
 44		return
 45	}
 46	clientName := r.Form.Get("client_name")
 47	redirectURI := r.Form.Get("redirect_uris")
 48	scopes := r.Form.Get("scopes")
 49	website := r.Form.Get("website")
 50	clientID := tokengen()
 51	clientSecret := tokengen()
 52	vapidKey := tokengen()
 53
 54	_, err := stmtSaveMastoApp.Exec(clientName, redirectURI, scopes, clientID, clientSecret, vapidKey, "")
 55	if err != nil {
 56		elog.Printf("error saving masto app: %v", err)
 57		http.Error(rw, "error saving masto app", http.StatusUnprocessableEntity)
 58		return
 59	}
 60
 61	j := junk.New()
 62	j["id"] = "19"
 63	j["website"] = website
 64	j["name"] = clientName
 65	j["redirect_uri"] = redirectURI
 66	j["client_id"] = clientID
 67	j["client_secret"] = clientSecret
 68	j["vapid_key"] = vapidKey
 69
 70	fmt.Println(j.ToString())
 71	goodjunk(rw, j)
 72}
 73
 74// https://docs.joinmastodon.org/methods/oauth/#authorize
 75func oauthorize(rw http.ResponseWriter, r *http.Request) {
 76	clientID := r.FormValue("client_id")
 77	redirectURI := r.FormValue("redirect_uri")
 78
 79	if !checkClientID(clientID) {
 80		elog.Println("oauth: no such client:", clientID)
 81		rw.WriteHeader(http.StatusUnauthorized)
 82		return
 83	}
 84
 85	var nrw NotResponseWriter
 86	login.LoginFunc(&nrw, r)
 87
 88	_, err := stmtSaveMastoAppToken.Exec(nrw.auth)
 89	if err != nil {
 90		elog.Println("oauth: failed to save masto app token", err)
 91		rw.WriteHeader(http.StatusInternalServerError)
 92		return
 93	}
 94
 95	uri := fmt.Sprintf("%s?code=%s", redirectURI, nrw.auth)
 96
 97	log.Println("redirecting to", uri)
 98	rw.Header().Set("Content-Type", "")
 99	rw.Header().Set("Location", uri)
100	rw.WriteHeader(302)
101}
102
103// https://docs.joinmastodon.org/methods/oauth/#token
104func oauthtoken(rw http.ResponseWriter, r *http.Request) {
105	grantType := r.FormValue("grant_type")
106	code := r.FormValue("code")
107	clientID := r.FormValue("client_id")
108	clientSecret := r.FormValue("client_secret")
109	redirectURI := r.FormValue("redirect_uri")
110	gotScope := r.FormValue("scope")
111
112	if !checkClient(clientID, clientSecret) {
113		elog.Println("oauth: no such client:", clientID)
114		rw.WriteHeader(http.StatusBadRequest)
115		return
116	}
117
118	app := MastoApp{}
119	row := stmtGetMastoApp.QueryRowx(clientID)
120	err := row.StructScan(&app)
121	if err == sql.ErrNoRows {
122		elog.Printf("oauth: invalid client: %s\n", clientID)
123		rw.WriteHeader(http.StatusBadRequest)
124		return
125	}
126	log.Printf("%#v", app)
127
128	possibleScopes := strings.Split(app.Scopes, " ")
129	requestedScopes := strings.Split(gotScope, " ")
130	for _, scope := range requestedScopes {
131		if !slices.Contains(possibleScopes, scope) {
132			elog.Printf("oauth: invalid scope: %s", scope)
133			rw.WriteHeader(http.StatusBadRequest)
134			return
135		}
136	}
137
138	if app.Scopes != gotScope {
139		elog.Printf("oauth: bad scopes: got %s; want %s", gotScope, app.Scopes)
140		rw.WriteHeader(http.StatusBadRequest)
141		return
142	}
143
144	if app.RedirectURI != redirectURI {
145		elog.Println("oauth: incorrect redirect URI")
146		rw.WriteHeader(http.StatusBadRequest)
147		return
148	}
149
150	if grantType == "authorization_code" {
151		// idk if this is ok? should code be reset?
152		if app.AuthToken == code {
153			accessToken := tokengen()
154			_, err := stmtSaveMastoAccessToken.Exec(app.ClientID, accessToken)
155			if err != nil {
156				elog.Println("oauth: failed to save masto access token", err)
157				rw.WriteHeader(http.StatusInternalServerError)
158				return
159			}
160			j := junk.New()
161			j["access_token"] = accessToken
162			j["token_type"] = "Bearer"
163			j["scope"] = app.Scopes
164			j["created_at"] = time.Now().UTC().Unix()
165			goodjunk(rw, j)
166		}
167	} else {
168		// gaslight the client
169		elog.Println("oauth: bad grant_type: must be authorization_code")
170		rw.WriteHeader(http.StatusBadRequest)
171		return
172	}
173}
174
175// https://docs.joinmastodon.org/methods/instance/#v2
176func instance(rw http.ResponseWriter, r *http.Request) {
177	j := junk.New()
178
179	var servername string
180	if err := getconfig("servername", &servername); err != nil {
181		http.Error(rw, "getting servername", http.StatusInternalServerError)
182		return
183	}
184
185	j["uri"] = servername
186	j["title"] = "honk"
187	j["description"] = "federated honk conveyance"
188	j["version"] = "develop"
189
190	thumbnail := junk.New()
191	thumbnail["url"] = fmt.Sprintf("https://%s/icon.png", servername)
192	j["thumbnail"] = thumbnail
193	j["languages"] = []string{"en"}
194
195	config := junk.New()
196
197	a := junk.New()
198	a["max_featured_tags"] = 10
199	config["accounts"] = a
200
201	s := junk.New()
202	s["max_characters"] = 5000
203	s["max_media_attachments"] = 1
204	s["characters_reserved_per_url"] = 23
205	config["statuses"] = s
206
207	m := junk.New()
208	m["supported_mime_types"] = []string{
209		"image/jpeg",
210		"image/png",
211		"image/gif",
212		"image/heic",
213		"image/heif",
214		"image/webp",
215		"image/avif",
216		"video/webm",
217		"video/mp4",
218		"video/quicktime",
219		"video/ogg",
220		"audio/wave",
221		"audio/wav",
222		"audio/x-wav",
223		"audio/x-pn-wave",
224		"audio/vnd.wave",
225		"audio/ogg",
226		"audio/vorbis",
227		"audio/mpeg",
228		"audio/mp3",
229		"audio/webm",
230		"audio/flac",
231		"audio/aac",
232		"audio/m4a",
233		"audio/x-m4a",
234		"audio/mp4",
235		"audio/3gpp",
236		"video/x-ms-asf",
237	}
238
239	m["image_size_limit"] = 10485760
240	m["image_matrix_limit"] = 16777216
241	m["video_size_limit"] = 41943040
242	m["video_frame_rate_limit"] = 60
243	m["video_matrix_limit"] = 2304000
244	j["media_attachments"] = m
245
246	goodjunk(rw, j)
247
248	return
249}
250
251//    /// The ID of the account.
252//    public let id: String
253//    /// The username of the account.
254//    public let username: String
255//    /// Equals username for local users, includes @domain for remote ones.
256//    public let acct: String
257//    /// The account's display name.
258//    public let displayName: String
259//    /// Biography of user.
260//    public let note: String
261//    /// URL of the user's profile page (can be remote).
262//    public let url: String
263//    /// URL to the avatar image.
264//    public let avatar: String
265//    /// URL to the avatar static image
266//    public let avatarStatic: String
267//    /// URL to the header image.
268//    public let header: String
269//    /// URL to the header static image
270//    public let headerStatic: String
271//    /// Boolean for when the account cannot be followed without waiting for approval first.
272//    public let locked: Bool
273//    /// The time the account was created.
274//    public let createdAt: String?
275//    /// The number of followers for the account.
276//    public var followersCount: Int
277//    /// The number of accounts the given account is following.
278//    public var followingCount: Int
279//    /// The number of statuses the account has made.
280//    public let statusesCount: Int
281//    /// An array of Emoji.
282//    public let emojis: [Emoji]
283//
284//    public let fields: [HashType]
285//
286//    /// A 3-5 word description of the account
287//    /// Optional, as this is a Mammoth addition
288//    public let summary: String?
289//
290//    public let bot: Bool
291//    public let lastStatusAt: String?
292//    public let discoverable: Bool?
293//
294//    public let source: AccountSource?
295
296// https://docs.joinmastodon.org/methods/accounts/#verify_credentials
297func verifycreds(rw http.ResponseWriter, r *http.Request) {
298	user, ok := somenumberedusers.Get(UserID(1))
299	if !ok {
300		elog.Fatalf("masto: no user number 1???")
301	}
302	j := junk.New()
303	j["id"] = fmt.Sprintf("%d", user.ID)
304	j["username"] = user.Name
305	j["created_at"] = ""
306	j["acct"] = user.Name
307	j["display_name"] = user.Options.CustomDisplay
308	j["url"] = user.URL
309	j["note"] = user.About
310	j["avatar"] = user.Options.Avatar
311	j["avatar_static"] = user.Options.Avatar
312	j["header"] = user.Options.Banner
313	j["header_static"] = user.Options.Banner
314	j["followers_count"] = 0
315	j["following_count"] = 0
316	j["statuses_count"] = getlocalhonkcount()
317	j["locked"] = false
318	j["bot"] = false
319	j["last_status_at"] = ""
320	j["discoverable"] = false
321	j["source"] = nil
322	j["emojis"] = []junk.Junk{}
323	j["fields"] = []junk.Junk{}
324
325	log.Println(j.ToString())
326
327	goodjunk(rw, j)
328}
329
330// https://docs.joinmastodon.org/methods/timelines/#home
331func hometimeline(rw http.ResponseWriter, r *http.Request) {
332	limit := r.URL.Query().Get("limit")
333	minid := r.URL.Query().Get("min_id")
334	maxid := r.URL.Query().Get("max_id")
335
336	dlog.Println("limit", limit, "minid", minid, "maxid", maxid)
337
338	var (
339		minidInt int
340		maxidInt int
341		limitInt int
342		err      error
343	)
344
345	list := []junk.Junk{}
346	me, ok := somenumberedusers.Get(UserID(1))
347	if !ok {
348		elog.Fatalf("masto: no user number 1???")
349	}
350
351	if limit == "" {
352		limit = "5"
353	}
354	limitInt, _ = strconv.Atoi(limit)
355
356	if minid != "" {
357		minidInt, err = strconv.Atoi(minid)
358		if err != nil {
359			elog.Printf("masto: invalid min_id: %s: %s", minid, err)
360			http.Error(rw, "invalid min_id", http.StatusBadRequest)
361			return
362		}
363	}
364	if maxid != "" {
365		maxidInt, err = strconv.Atoi(maxid)
366		if err != nil {
367			elog.Printf("masto: invalid max_id: %s: %s", minid, err)
368			http.Error(rw, "invalid max_id", http.StatusBadRequest)
369			return
370		}
371	}
372
373	honks := getlimitedhonksforuser(me.ID, int64(minidInt), int64(maxidInt), limitInt)
374	reverbolate(me.ID, honks)
375	for _, h := range honks {
376		hj := honktomasto(h)
377		if hj == nil {
378			elog.Printf("masto: failed to convert honk %d to masto", h.ID)
379			http.Error(rw, "failed to convert honk", http.StatusInternalServerError)
380		}
381		list = append(list, hj)
382	}
383
384	buf := bytes.Buffer{}
385	err = listjunk(&buf, list)
386	if err != nil {
387		elog.Println(err)
388		http.Error(rw, "error encoding json", http.StatusInternalServerError)
389	}
390
391	rw.Header().Set("Content-Type", "application/json; charset=utf-8")
392	rw.WriteHeader(http.StatusOK)
393	rw.Write(buf.Bytes())
394}